import {countColumn} from "../util/misc.js"
import {copyState, innerMode, startState} from "../modes.js"
import StringStream from "../util/StringStream.js"

import {getLine, lineNo} from "./utils_line.js"
import {clipPos} from "./pos.js"

class SavedContext {
    constructor(state, lookAhead) {
        this.state = state
        this.lookAhead = lookAhead
    }
}

class Context {
    constructor(doc, state, line, lookAhead) {
        this.state = state
        this.doc = doc
        this.line = line
        this.maxLookAhead = lookAhead || 0
        this.baseTokens = null
        this.baseTokenPos = 1
    }

    static fromSaved(doc, saved, line) {
        if (saved instanceof SavedContext)
            return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead)
        else
            return new Context(doc, copyState(doc.mode, saved), line)
    }

    lookAhead(n) {
        let line = this.doc.getLine(this.line + n)
        if (line != null && n > this.maxLookAhead) this.maxLookAhead = n
        return line
    }

    baseToken(n) {
        if (!this.baseTokens) return null
        while (this.baseTokens[this.baseTokenPos] <= n)
            this.baseTokenPos += 2
        let type = this.baseTokens[this.baseTokenPos + 1]
        return {
            type: type && type.replace(/( |^)overlay .*/, ""),
            size: this.baseTokens[this.baseTokenPos] - n
        }
    }

    nextLine() {
        this.line++
        if (this.maxLookAhead > 0) this.maxLookAhead--
    }

    save(copy) {
        let state = copy !== false ? copyState(this.doc.mode, this.state) : this.state
        return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state
    }
}


// Compute a style array (an array starting with a mode generation
// -- for invalidation -- followed by pairs of end positions and
// style strings), which is used to highlight the tokens on the
// line.
export function highlightLine(cm, line, context, forceToEnd) {
    // A styles array always starts with a number identifying the
    // mode/overlays that it is based on (for easy invalidation).
    let st = [cm.state.modeGen], lineClasses = {}
    // Compute the base array of styles
    runMode(cm, line.text, cm.doc.mode, context, (end, style) => st.push(end, style),
        lineClasses, forceToEnd)
    let state = context.state

    // Run overlays, adjust style array.
    for (let o = 0; o < cm.state.overlays.length; ++o) {
        context.baseTokens = st
        let overlay = cm.state.overlays[o], i = 1, at = 0
        context.state = true
        runMode(cm, line.text, overlay.mode, context, (end, style) => {
            let start = i
            // Ensure there's a token end at the current position, and that i points at it
            while (at < end) {
                let i_end = st[i]
                if (i_end > end)
                    st.splice(i, 1, end, st[i + 1], i_end)
                i += 2
                at = Math.min(end, i_end)
            }
            if (!style) return
            if (overlay.opaque) {
                st.splice(start, i - start, end, "overlay " + style)
                i = start + 2
            } else {
                for (; start < i; start += 2) {
                    let cur = st[start + 1]
                    st[start + 1] = (cur ? cur + " " : "") + "overlay " + style
                }
            }
        }, lineClasses)
        context.state = state
        context.baseTokens = null
        context.baseTokenPos = 1
    }

    return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}
}

export function getLineStyles(cm, line, updateFrontier) {
    if (!line.styles || line.styles[0] != cm.state.modeGen) {
        let context = getContextBefore(cm, lineNo(line))
        let resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state)
        let result = highlightLine(cm, line, context)
        if (resetState) context.state = resetState
        line.stateAfter = context.save(!resetState)
        line.styles = result.styles
        if (result.classes) line.styleClasses = result.classes
        else if (line.styleClasses) line.styleClasses = null
        if (updateFrontier === cm.doc.highlightFrontier)
            cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier)
    }
    return line.styles
}

export function getContextBefore(cm, n, precise) {
    let doc = cm.doc, display = cm.display
    if (!doc.mode.startState) return new Context(doc, true, n)
    let start = findStartLine(cm, n, precise)
    let saved = start > doc.first && getLine(doc, start - 1).stateAfter
    let context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start)

    doc.iter(start, n, line => {
        processLine(cm, line.text, context)
        let pos = context.line
        line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null
        context.nextLine()
    })
    if (precise) doc.modeFrontier = context.line
    return context
}

// Lightweight form of highlight -- proceed over this line and
// update state, but don't save a style array. Used for lines that
// aren't currently visible.
export function processLine(cm, text, context, startAt) {
    let mode = cm.doc.mode
    let stream = new StringStream(text, cm.options.tabSize, context)
    stream.start = stream.pos = startAt || 0
    if (text == "") callBlankLine(mode, context.state)
    while (!stream.eol()) {
        readToken(mode, stream, context.state)
        stream.start = stream.pos
    }
}

function callBlankLine(mode, state) {
    if (mode.blankLine) return mode.blankLine(state)
    if (!mode.innerMode) return
    let inner = innerMode(mode, state)
    if (inner.mode.blankLine) return inner.mode.blankLine(inner.state)
}

function readToken(mode, stream, state, inner) {
    for (let i = 0; i < 10; i++) {
        if (inner) inner[0] = innerMode(mode, state).mode
        let style = mode.token(stream, state)
        if (stream.pos > stream.start) return style
    }
    throw new Error("Mode " + mode.name + " failed to advance stream.")
}

class Token {
    constructor(stream, type, state) {
        this.start = stream.start;
        this.end = stream.pos
        this.string = stream.current()
        this.type = type || null
        this.state = state
    }
}

// Utility for getTokenAt and getLineTokens
export function takeToken(cm, pos, precise, asArray) {
    let doc = cm.doc, mode = doc.mode, style
    pos = clipPos(doc, pos)
    let line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise)
    let stream = new StringStream(line.text, cm.options.tabSize, context), tokens
    if (asArray) tokens = []
    while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
        stream.start = stream.pos
        style = readToken(mode, stream, context.state)
        if (asArray) tokens.push(new Token(stream, style, copyState(doc.mode, context.state)))
    }
    return asArray ? tokens : new Token(stream, style, context.state)
}

function extractLineClasses(type, output) {
    if (type) for (; ;) {
        let lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/)
        if (!lineClass) break
        type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length)
        let prop = lineClass[1] ? "bgClass" : "textClass"
        if (output[prop] == null)
            output[prop] = lineClass[2]
        else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
            output[prop] += " " + lineClass[2]
    }
    return type
}

// Run the given mode's parser over a line, calling f for each token.
function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) {
    let flattenSpans = mode.flattenSpans
    if (flattenSpans == null) flattenSpans = cm.options.flattenSpans
    let curStart = 0, curStyle = null
    let stream = new StringStream(text, cm.options.tabSize, context), style
    let inner = cm.options.addModeClass && [null]
    if (text == "") extractLineClasses(callBlankLine(mode, context.state), lineClasses)
    while (!stream.eol()) {
        if (stream.pos > cm.options.maxHighlightLength) {
            flattenSpans = false
            if (forceToEnd) processLine(cm, text, context, stream.pos)
            stream.pos = text.length
            style = null
        } else {
            style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses)
        }
        if (inner) {
            let mName = inner[0].name
            if (mName) style = "m-" + (style ? mName + " " + style : mName)
        }
        if (!flattenSpans || curStyle != style) {
            while (curStart < stream.start) {
                curStart = Math.min(stream.start, curStart + 5000)
                f(curStart, curStyle)
            }
            curStyle = style
        }
        stream.start = stream.pos
    }
    while (curStart < stream.pos) {
        // Webkit seems to refuse to render text nodes longer than 57444
        // characters, and returns inaccurate measurements in nodes
        // starting around 5000 chars.
        let pos = Math.min(stream.pos, curStart + 5000)
        f(pos, curStyle)
        curStart = pos
    }
}

// Finds the line to start with when starting a parse. Tries to
// find a line with a stateAfter, so that it can start with a
// valid state. If that fails, it returns the line with the
// smallest indentation, which tends to need the least context to
// parse correctly.
function findStartLine(cm, n, precise) {
    let minindent, minline, doc = cm.doc
    let lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100)
    for (let search = n; search > lim; --search) {
        if (search <= doc.first) return doc.first
        let line = getLine(doc, search - 1), after = line.stateAfter
        if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier))
            return search
        let indented = countColumn(line.text, null, cm.options.tabSize)
        if (minline == null || minindent > indented) {
            minline = search - 1
            minindent = indented
        }
    }
    return minline
}

export function retreatFrontier(doc, n) {
    doc.modeFrontier = Math.min(doc.modeFrontier, n)
    if (doc.highlightFrontier < n - 10) return
    let start = doc.first
    for (let line = n - 1; line > start; line--) {
        let saved = getLine(doc, line).stateAfter
        // change is on 3
        // state on line 1 looked ahead 2 -- so saw 3
        // test 1 + 2 < 3 should cover this
        if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) {
            start = line + 1
            break
        }
    }
    doc.highlightFrontier = Math.min(doc.highlightFrontier, start)
}
